<!DOCTYPE html>
<html lang="zh-CN">
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title>
        
            Go 实际项目测试实践
        
    </title>
    <link rel="icon" href="/img/favicon.png"/>
    
<link rel="stylesheet" href="/css/style.css">

    
<link rel="stylesheet" href="/css/font-awesome.min.css">

    <!-- 
<link rel="stylesheet" href="/css/gitment.css">
 -->
    
<link rel="stylesheet" href="/css/hljs.min.css">

    
<script src="/js/hljs.min.js"></script>
  
    <script>
        var _hmt = _hmt || [];
        (function() {
          var hm = document.createElement("script");
          hm.src = "https://hm.baidu.com/hm.js?8ad9e7d37411eee585b4bd7194806394";
          var s = document.getElementsByTagName("script")[0]; 
          s.parentNode.insertBefore(hm, s);
        })();
    </script>  
    <!-- 
<script src="/js/gitment.browser.js"></script>
   -->
    <link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css">
    <script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>
<meta name="generator" content="Hexo 5.3.0"><link rel="alternate" href="/atom.xml" title="Nick" type="application/atom+xml">
</head>
<body>
    <header class="header" id="header">
    <h1>
        <a class="title" href="/">
            Nick
        </a>
    </h1>
    <h2>
        <a class="motto">
            天地间的怪事莫过于侦察别人的一些和自己绝不相干的事了
        </a>
    </h2>
    <nav class="navbar">
        <ul class="menu">
            
            
                <li class="menu-item">
                    <a href="/" class="menu-item-link">
                        首页
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a href="/archives" class="menu-item-link">
                        归档
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a target="_blank" rel="noopener" href="https://github.com/StoneQI" class="menu-item-link">
                        Github
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a href="/atom.xml" class="menu-item-link">
                        RSS
                    </a>
                </li>
            
                
            
                
            
                <li class="menu-item">
                    <a href="/category-archive" class="menu-item-link">
                        目录
                    </a>
                </li>
            
                
                
                <li class="menu-item">
                    <a class="menu-item-link search">
                        Search                   
                        <i class="fa fa-long-arrow-right search-icon" aria-hidden="true"></i>
                    </a>
                        <input placeholder="Search..." class="search-input" style="display:none;border:none!important;" onkeydown="onEnter(event)" onkeypress="onEnter(event)"></input>
                </li>
                
        </ul>
    </nav>
</header>
    <main class="main">
        <article class="post">
            <h1>
                <a class="title" href="/2021/02/22/Go-Test-Used/"> 
                    Go 实际项目测试实践 
                </a>
            </h1>
            <div class="meta">
                <a class="date"> 
                    <i class="fa fa-calendar" aria-hidden="true"></i>                    
                    2021-02-22   
                </a>
                
                <a class="category">
                    <i class="fa fa-th" aria-hidden="true"></i>  
                </a>
               
                <ul class="category-list"><li class="category-list-item"><a class="category-list-link" href="/categories/go/">go</a></li></ul>
                
                <a class="tag">
                    <i class="fa fa-tags" aria-hidden="true"></i>  
                </a>
                
                    <ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/tags/go/" rel="tag">go</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E6%B5%8B%E8%AF%95/" rel="tag">测试</a></li></ul>
            </div>
<div class="toc">
  <ol class="toc-list"><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E9%A1%B9%E7%9B%AE%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%BB%8D"><span class="toc-list-text">项目基本介绍</span></a></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E5%9F%BA%E7%A1%80%E4%BE%9D%E8%B5%96%E5%B1%82%E6%B5%8B%E8%AF%95"><span class="toc-list-text">基础依赖层测试</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#DAO-%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BE%9D%E8%B5%96%E5%B1%82%E6%B5%8B%E8%AF%95"><span class="toc-list-text">DAO 数据库依赖层测试</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E7%BC%93%E5%AD%98%E5%B1%82%E3%80%81RPC%E5%B1%82%E7%AD%89%E5%9F%BA%E7%A1%80%E4%BE%9D%E8%B5%96"><span class="toc-list-text">缓存层、RPC层等基础依赖</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#Service%E5%B1%82%E6%B5%8B%E8%AF%95"><span class="toc-list-text">Service层测试</span></a></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#API%E5%B1%82%E6%B5%8B%E8%AF%95"><span class="toc-list-text">API层测试</span></a></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E4%B8%80%E4%BA%9B%E6%B5%8B%E8%AF%95%E9%97%AE%E9%A2%98"><span class="toc-list-text">一些测试问题</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E5%A4%9A%E4%B8%AA%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E5%A6%82%E4%BD%95%E5%86%99%E6%B5%8B%E8%AF%95"><span class="toc-list-text">多个函数调用如何写测试</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E6%9C%89%E7%8A%B6%E6%80%81%E5%87%BD%E6%95%B0%E5%A6%82%E4%BD%95%E6%B5%8B%E8%AF%95"><span class="toc-list-text">有状态函数如何测试</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E6%97%A0%E6%B3%95mock%E7%9A%84%E4%BE%9D%E8%B5%96"><span class="toc-list-text">无法mock的依赖</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%80%BB%E7%BB%93"><span class="toc-list-text">总结</span></a></li></ol>
</div>
            <div class="content">
                <p>测试的重要性不言而喻。然而在项目中，真正做到编写完整的测试却是少之又少。</p>
<p>主要原因推测有三个：</p>
<ul>
<li>觉得写测试太麻烦，花费时间。其实我觉得这是个伪命题，因为通常写完一段代码，我们肯定会测试该代码能否正确执行，比如HTTP请求，数据库请求，RPC请求，逻辑函数。我们都会进行访问测试，这其实就是测试用例了，只是没有集成到测试框架中。其实，我们完全可以将这部分测试写成代码放到测试中。</li>
<li>代码不可测试。这其实很常见，大部分我们都是接手已有的代码。如果代码没有好的架构设计，统一的分格，强制的CR。那么写出不可测试的代码也就司空见惯了。对于这种代码，只能进行逐步重构了。</li>
<li>不知道怎么测试。项目中的代码一般都会有很多的依赖，而测试中去独立部署这些服务肯定成本巨大，因此如何处理这些依赖是建立测试的最大障碍。下文中我也会尝试介绍这部分的处理手段。</li>
</ul>
<p>一般的测试分为单元测试和集成测试，但是这里我们会模糊这种界限，不做具体区分。下面出现的代码都是为了这篇文章而写的伪代码，没有考虑架构之类的。</p>
<a id="more"></a>
<h1 id="项目基本介绍"><a href="#项目基本介绍" class="headerlink" title="项目基本介绍"></a>项目基本介绍</h1><p>为了结合项目介绍测试的方法，在这里，我简单实现了一个基本的业务逻辑框架。分为API层、Service层，基础依赖层有数据库（也就是DAO层）、缓存层（一般也就是Redis）、RPC等。简单代码函数如下：</p>
<pre><code class="go">// Service 为业务领域。经常变动大，因此不做接口
type PostService struct&#123;
    // Service 通过属性保存用到的基础依赖，通过依赖注入进行解藕，一能灵活改变扩展，二也方便测试MOCK
    // PostDAO 和 RedisClient 皆为接口，变动会很小。
    postDAO *PostDAO  // 数据库依赖，后面会细讲
    cacheClient *CacheClient // 缓存层依赖，后面会细讲
&#125;

// 依赖注入，也叫控制反转，
func NewPostService(postDAO *PostDAO, redisClient *RedisClient)  &#123;
    return &amp;PostService&#123;
        postDAO: postDAO,
        cacheClient: cacheClient,
    &#125;    
&#125; 
// 具体的业务接口
func (post *PostService)GetAllPost()  &#123;
    ... 业务逻辑
    post.postDAO.FindPosts()
    ....
&#125; 

// API层（也可叫Controller层）
type Server struct&#123;
    // go 中 http服务器对象
    ln     net.Listener
    server *http.Server 
    
    // 对应的 Service 逻辑，启动应用时实例化
    postService *PostService
&#125;

func NewServer(ln net.Listener, server *http.Server )  &#123;
    return &amp;Server&#123;
        ln: ln,
        server: server,
    &#125;    
&#125; 
// 依赖注入 PostService
func (server *Server)SetPostService(postDAO *PostDAO, redisClient *RedisClient)  &#123;
    server.postService =  NewPostService(postDAO, redisClient)
&#125; 
// API
func (server *Server)GetPosts(w http.ResponseWriter, r *http.Request)  &#123;
    ... 请求参数检查
    server.postService.GetAllPost()
    ... 返回响应信息
&#125; 
</code></pre>
<p> 上面的设计算是一种 领域驱动设计（DDD）（其实和MVC等传统的分层差不了太多），业务逻辑层和所有的其他层进行了解藕，更换底层的数据库，缓存层时，不需要改变业务逻辑代码。常见的架构拆分有模块拆分 和分层拆分，这里不做扩展，简单提供个小经验。大项目可以模块拆分，小项目分层拆分就行，另外由于Go的循环引用限制，大多数情况下分层拆分更好，具体可看 <a href="https://stoneqi.github.io/2021/01/25/tr-packages-as-layers-not-groups/">翻译Packages as layers, not groups</a>。</p>
<p>上述业务架构机会适合大部分项目，根据代码我们可以将测试分为三部分：一是API层测试；二是Service测试；三是依赖层测试。</p>
<p>虽然访问是从API到依赖层，但是测试中首先要讲解的便是依赖层。</p>
<h1 id="基础依赖层测试"><a href="#基础依赖层测试" class="headerlink" title="基础依赖层测试"></a>基础依赖层测试</h1><p>基础依赖层即好测试又不好测试，好测试是应为依赖层不会在依赖其他层，同时代码变动不大，开发完成几乎不会变动。不好测试是因为基础依赖机会都需要开启另一个服务，比如MySQL，Redis同时还的进行初始化。</p>
<h2 id="DAO-数据库依赖层测试"><a href="#DAO-数据库依赖层测试" class="headerlink" title="DAO 数据库依赖层测试"></a>DAO 数据库依赖层测试</h2><p>测试的基础是DAO层需要解藕，也就是得建立对应的接口文件（由于基层依赖层变动少，因此接口必须仔细设计好，<a href="https://stoneqi.github.io/2021/01/24/tr-common-crud-design-in-go/">翻译Common CRUD Design in Go</a>可以参考），同时进行依赖反转。虽然在Go中不通过接口也能去直接MOCK对应函数，但是极不推荐，本文也不会做详细说明。</p>
<p>DAO层主要是进行数据库中的表格增删改查的操作。下面实现上述代码的PostDAO接口。</p>
<pre><code class="go">// 接口文件
type PostDAO interface &#123;

    FindPostByID(ctx context.Context, id int) (*Post, error)

    FindPosts(ctx context.Context, filter PostFilter) ([]*Post, int, error)

    CreatePost(ctx context.Context, post *Post) error

    UpdatePost(ctx context.Context, id int, upd PostUpdate) (*Post, error)

    DeletePost(ctx context.Context, id int) error
&#125;

// 对应的实现
type PostMysql struct&#123;
    db *sql.DB
&#125;
// 依赖注入 sql.DB
func NewPostMysql(db *sql.DB)&#123;
    return &amp;PostMysql&#123;
        db: db,
    &#125;
&#125;


func (s *PostMysql) FindPostByID(ctx context.Context, id int) (*Post, error)&#123;
    ...
&#125;

func (s *PostMysql) FindPosts(ctx context.Context, filter PostFilter) ([]*Post, int, error)&#123;
    s.db.Exec(&quot;...&quot;,...)
&#125;

func (s *PostMysql) CreatePost(ctx context.Context, post *Post) error&#123;
    ...
&#125;

func (s *PostMysql) UpdatePost(ctx context.Context, id int, upd PostUpdate) (*Post, error)&#123;
    ...
&#125;

func (s *PostMysql) DeletePost(ctx context.Context, id int) error
&#123;
    ...
&#125;
</code></pre>
<p>DAO层测试有一个重要的问题值得讨论：应不应该建一个数据库进行测试。建立数据库能最好的模拟真实环境，但是成本巨大。不建立进行Mock（这里的Mock是只mock掉DB数据库，而不是DAO这层）的话，也有问题，一是未在真实环境中进行测试，难免会有问题，而是mock掉一个数据成本不必建立一个数据库大。在这里，我是推荐建立真实数据库的，传统建立数据库麻烦，但是幸好，我们又Docker 和 docker-compose （还不知道docker的可以退群了）。一共三种方案依次介绍</p>
<ol>
<li>建立真实数据库，测试最方便。直接上<code>docker-compose.yml</code>代码。通过docker-compose 我们可以一条命令启动我们的测试环境，并且用完就可以删除容器。完美无害<br>```yml<br>version: “3.9”</li>
</ol>
<p>services:<br>    mysql:<br>    networks:<br>        - mysql_net<br>    image: mysql:5.7.29<br>    restart: always<br>    command: –default-authentication-plugin=mysql_native_password<br>    environment:<br>        MYSQL_ROOT_PASSWORD: test123<br>    ports:<br>        - 3306:3306<br>    volumes:<br>        # mysql 初始化脚本目录，可以将init.sql放入该目录，容器初始化时会自动执行。<br>        - “./docker/init_sql:/docker-entrypoint-initdb.d/“</p>
<pre><code># 查看mysql 工具，网页访问 8080 可查看数据库内容。
adminer:
networks:
    - mysql_net
image: adminer
restart: always
ports:
    - 8080:8080
</code></pre>
<p>networks:<br>    mysql_net:<br>    name: mysql_net<br>    driver: bridge</p>
<pre><code>   
   测试代码
   
```go
func TestPostMysql(t *testing.T) &#123;

// 连接 docker 数据库
conn_str := &quot;root:test123@tcp(localhost:3306)/dataname?autocommit=1&amp;timeout=200ms&amp;readTimeout=2s&amp;writeTimeout=2s&quot;
db, err := sql.Open(&quot;mysql&quot;,conn_str)
    if err != nil &#123;
        log.Fatal(err)
    &#125;
    defer db.Close()
    
    postMysql := NewPostMysql(db)
    // 使用了Convey 框架
convey.Convey(&quot;PostMysql 测试&quot;,t, func() &#123;
    post := &amp;Post&#123;
                ...
        &#125;
    convey.Convey(&quot;添加-&gt;查找-&gt;删除测试&quot;, func() &#123;
        convey.Convey(&quot;添加数据&quot;, func() &#123;
            postNew := post
            err := postMysql.CreatePost(context.Background(), postNew)
            convey.So(err, convey.ShouldEqual, nil)

            convey.Convey(&quot;查找&quot;, func() &#123;
                res, err :=postMysql.FindPostByID(context.Background(),postNew.PostId)
                convey.So(err,convey.ShouldEqual,nil)
                convey.So(*res,convey.ShouldResemble,postNew)

                convey.Convey(&quot;删除&quot;, func() &#123;
                    err = postMysql.DeletePost(context.Background(),postNew.PostId)
                    convey.So(err, convey.ShouldEqual, nil)

                        convey.Convey(&quot;再查找试试&quot;, func() &#123;
                        _, err :=postMysql.FindPostByID(context.Background(),postNew.PostId)
                        convey.So(err,convey.ShouldBeError)
                &#125;)
            &#125;)

        &#125;)
    &#125;)

&#125;)
&#125;
</code></pre>
<p>测试代码例子， <code>docker-compose.yml</code>都给出来了，相信大家都能看懂，也就不多说了。<br>2. 第二种方法是用内存数据库，在Java中有h2内存数据库，不见建立第三方服务，且兼容大部分Mysql语法。在Go里面，由于 <code>database/sql</code>是一个接口层，因此，我们可以采用Sqlite的驱动实例化一个<code>sql.DB</code>出来，但是还是会有兼容问题，因此极不推荐。使用在上述代码中改个驱动就行了，也就不详细说明了。<br>3. 第三种方法是Go独有的，也是因为<code>database/sql</code>是一个接口层，有一个包<code>go-sqlmock</code>，其实和第二种也是一样的，只是需要自己模拟输入输出，具体可以看官网用法。也不推荐。</p>
<h2 id="缓存层、RPC层等基础依赖"><a href="#缓存层、RPC层等基础依赖" class="headerlink" title="缓存层、RPC层等基础依赖"></a>缓存层、RPC层等基础依赖</h2><p>在应用中缓存层是少不了的。现在的缓存大部分都是Redis，类似一个内存数据库，因此测试方法和上述一样，推荐在Docker中真实的建立一个Redis服务，如果实在不能建立，可以考虑使用 <a target="_blank" rel="noopener" href="https://github.com/alicebob/miniredis">miniredis</a>项目，和上述DAO中的第二种方法类似，模拟了一个小的Redis，兼容大部分常见命令。</p>
<p>在这里值得说的是RPC等依赖。在微服务中，一个服务调用多个服务是很常见的情况。而这些服务我们又不可能在本地建立一个真实的环境。因此只能想办法去Mock掉。方法也很简单，就是根据服务提供的API抽象出一个接口文件，然后使用适配器模式或代理模式进行Wrap一层。</p>
<h1 id="Service层测试"><a href="#Service层测试" class="headerlink" title="Service层测试"></a>Service层测试</h1><p>Sevice层通常是业务逻辑的地方，这里的测试容易与否主要看基础依赖层和Service是否完全解藕，如果解藕，Service层的测试将会变得非常容易。</p>
<p>这里也有一个问题，是否要对基础依赖层进行Mock，有人可能问，不Mock的话直接设计接口这一套是干嘛呢，不如直接搭建环境。这里的主要不Mock的原因是我们想尽量真实的模拟线上环境。而且有些基础设计是只能Mock的。同时，这里可以不Mock的另一个前提是，我们的解藕做的很好，同时基层依赖层的测试也做的很好。不然的话我们只能进行Mock了。</p>
<p>常用的Mock工具是 <code>go-mock</code>具体用法不多介绍了，可以直接看文档。我简单说下，我的做法，由于解藕做的很好，所以我的测试实现中会自动判断是否数据库能连接成功，如何可以的话用数据库实例的基础依赖层测试，否则用<code>go-mock</code>生成的基础依赖层进行测试。具体代码如下</p>
<pre><code class="go">
func TestGetAllPost(t1 *testing.T) &#123;
    postDAO,issql:= GetPostDAO(t1)
    if !issql &#123;
        ... mock 配置
    &#125;
    // redisClient 的获取方法一样，不写代码

    edisClient,isTrue:= GetRedisClient(t)
    if !isTrue &#123;
        ... mock 初使化
        
    &#125;
    postService := NewPostService(postDAO, redisClient)
    Convey(&quot;TestGetAllPost&quot;, t1, func() &#123;
        postServie.GetAllPost()
        ...
    &#125;)
&#125;

func GetPostDAO(t1 *testing.T) (PostDao,bool)&#123;
    conn_str := &quot;root:test123@tcp(localhost:3306)/rule?autocommit=1&amp;timeout=200ms&amp;readTimeout=2s&amp;writeTimeout=2s&quot;
    db, err := sql.Open(&quot;mysql&quot;,conn_str)
    if err != nil &#123;
        log.Fatal(err)
    &#125;
    if err := conn.Ping(); err != nil &#123;
        return nil,false
    &#125;

    if falg &#123;
        t1.Log(&quot;数据库开启，使用数据库进行测试&quot;)
        return NewPostMysql(db),true
    &#125;else &#123;
        t1.Log(&quot;数据库未开启，使用MOCK DAO层测试&quot;)
        ctl := gomock.NewController(t1)
        defer ctl.Finish()
        PostDaoMock := mock.NewMockPostDao(ctl)
        //TemplateId := 123456
        //MustMockCreateTemplate(templateDaoMock,TemplateId)
        return PostDao,false
    &#125;

&#125;

</code></pre>
<h1 id="API层测试"><a href="#API层测试" class="headerlink" title="API层测试"></a>API层测试</h1><p>API层测试可以说是集成测试了，一般开发人员开发后，测试人员主要会对这部分接口进行测试。而且这部分测试一般都需要建立HTTP请求，因此耗时费力，开发人员大部分只会进行简单的访问看看能够跑通。然而Go给我们提供了方便的http测试工具，使用这些工具不但能方便测试，还能保存复用，不必每次在浏览器测试。下面代码举个例子。</p>
<pre><code class="go">func setupServerPostHandler(t *testing.T) *gin.Engine &#123;
    engine := gin.New()
    //engine.Use(middleware.Logger())
    engine.Use(gin.Recovery())
    PostDaoMock,issql:= GetPostDAO(t)
    if !issql &#123;
        ... mock 初使化
        
    &#125;
    RedisClient,isTrue:= GetRedisClient(t)
    if !isTrue &#123;
        ... mock 初使化
        
    &#125;
    server := NewServer()
    // 唯一依赖
    server.SetPostService(PostDaoMock,RedisClient)
    
    engine.POST(&quot;/AddPost&quot;, server.GetPosts)
    return engine
&#125;

func TestPostHandler(t *testing.T) &#123;
    router := setupServerPostHandler(t)
    Convey(&quot;Post Handler接口测试&quot;,t, func() &#123;
        req_content := &amp;Post&#123;
            ... 内容
        &#125;
        type Data_resp struct &#123;
            Post_id int `json:Post_id`
        &#125;
        type resp_json struct &#123;
            Data  Data_resp `json:data`
            Err_msg  string `json:err_msg`
            Err_no   int `json:err_no`
        &#125;

        req_content.Type = &quot;AddPost&quot;
        Convey(&quot;AddPost 测试&quot;, func() &#123;

            Convey(&quot;AddPost 测试1&quot;, func() &#123;

                req_new := req_content
                req_string, _ := json.Marshal(req_new)
                req := httptest.NewRequest(http.MethodPost, &quot;/AddPost&quot;, strings.NewReader(string(req_string)))
                req.Header[global.HEADER_TRACEID] = []string&#123;&quot;testTrace&quot;&#125;
                req.Header[global.HEADER_SPANID] = []string&#123;&quot;testSpan&quot;&#125;
                req.Header[global.HEADER_USER] = []string&#123;&quot;testUser&quot;&#125;
                req.Header.Set(&quot;Content-Type&quot;,&quot;application/json&quot;)

                w := httptest.NewRecorder()
                router.ServeHTTP(w, req)
                resp := w.Result()
                resp_json1 := &amp;resp_json&#123;&#125;
                _ = json.Unmarshal(w.Body.Bytes(), resp_json1)
                So(resp_json1.Data.Post_id,ShouldHaveSameTypeAs,1)
                So(resp.StatusCode,ShouldEqual,http.StatusOK)
            &#125;)
        &#125;)
    &#125;)
&#125;
</code></pre>
<p>相信代码已经足够清晰，因此不在赘述。</p>
<h1 id="一些测试问题"><a href="#一些测试问题" class="headerlink" title="一些测试问题"></a>一些测试问题</h1><h2 id="多个函数调用如何写测试"><a href="#多个函数调用如何写测试" class="headerlink" title="多个函数调用如何写测试"></a>多个函数调用如何写测试</h2><p>通常函数直接是相互调用的，比如API层调用Service层，接着调用基础服务层。一个请求在多个函数中经过。这种情况各个测试的侧重点如何写呢。</p>
<p>有个简单做法，各自函数着重测试自己的实现逻辑。比如一般在API层，我们会进行参数检测，那在这一步我们就着重测试参数检测的测试用例。而在Service，此时我们只需要提供正确的参数就行，不必在提供错误的请求参数。而在基础依赖层，我们也就不必测试ID小于0，或参数类型，范围的错误用例了。</p>
<h2 id="有状态函数如何测试"><a href="#有状态函数如何测试" class="headerlink" title="有状态函数如何测试"></a>有状态函数如何测试</h2><p>什么叫有状态函数，这里简单顶一下，函数中出现的可变变量不是由参数传递的。比如函数中如果用了全局变量，类的中局部属性，时间函数，随机数生成器，那么这些就都是有状态的，在不同时间、环境下，相同的输入会有不同的输出。当然也有一些情况例外，比如上述中DAO中的sql.DB一般初始化后就不会改变，这种就不算有状态函数了。</p>
<p>这些函数在测试之前，我们必须固定其中的参数，常见的语言中，你必须将这些方法单独提取出来，进行封装。然而在Go中，你可以使用<code>monkey</code>   包直接在运行时替换掉原函数。该库为<code>github.com/agiledragon/gomonkey</code>，具体用法可以查看官方文档。</p>
<h2 id="无法mock的依赖"><a href="#无法mock的依赖" class="headerlink" title="无法mock的依赖"></a>无法mock的依赖</h2><p>一些项目由于历史原因，无法解藕，这部分代码，如果非要测试，也只有使用上述的<code>monkey</code>直接替换原函数。</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>给一个函数写测试其实并不难，但是给一个大项目写测试需要一定的架构设计能力，首先的理解项目的架构，然后才能围绕项目设计一个测试的架构。大部分人化时间去学习MVC，分层等架构，但是很少会有些去学习测试框架的架构。而者也是大部分人面对一个项目不知道如何去写测试的主要原因。希望这篇文章能给大家一点收获。</p>
<p>另外，测试也是不断集成的，我们不可能使用测试一下子就测试到所有情况，因此在线上环境发现 bug 时，需要及时补充我们的测试。这样，不断集成测试用例后，后续开发时，就真正只需要测试一下，就可以放心的提交上线。</p>

            </div>
          
           
            <div class="copyright">
                <div class="name">
                    <a>本文作者:</a>
                    <a>Nick Yang</a>
                </div>
                <div class="link">
                    <a>本文链接:</a>
                    <a class="permalink" href="https://stoneqi.github.io/2021/02/22/Go-Test-Used/">https://stoneqi.github.io/2021/02/22/Go-Test-Used/</a>
                </div>
                <div class="license">
                    <a>版权声明:</a>
                    <a>本博客所有文章除特别声明外，均采用许可协议 CC BY-NC-SA 3.0 CN。转载请注明出处！</a>
                </div>
            </div>
            

          


</article>


<div class="tip">
<button class="tip-btn">
    打赏
</button>
<div class="tip-img">
<ul>
    
<li>
    <img src="/img/aipay.jpg"></img>
</li>

 
<li>
    <img src="/img/wechatpay.png"></img>
</li>

</ul>
</div>
</div>

<div class="more">

    <div class="prev">
   <a href="/2021/02/28/hello-world/">  hello world</a>
    </div>

<div></div>

    <div class="next">
    <a href="/2021/01/27/Real-world-SQL-in-Go-Part-I/"> [翻译]Real-world SQL in Go: PartI </a>
    </div>
    
</div>

<div class="bdsharebuttonbox">
<a href="#" class="bds_weixin fa fa-weixin" data-cmd="weixin" title="分享到微信" style="color:#1cbd8f">
</a>
<a href="#" class="bds_tsina fa fa-weibo" data-cmd="tsina" title="分享到新浪微博" style="color:#ff6363">
</a>
<a href="#" class="bds_twi fa fa-twitter" data-cmd="twi" title="分享到Twitter" style="color:#00A7EB">
</a>
<a href="#" class="bds_fbook fa fa-facebook" data-cmd="fbook" title="分享到Facebook" style="color:#00A7EB">
</a>
</div>
<script>
window._bd_share_config={
    "common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"1","bdSize":"16"},
    "share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='../../../../../static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];
</script>
 
<!-- <div id="comments"></div> -->
<div id="gitalk-container"></div>

<script>
  //   var gitment = new Gitment({
  //   id: "Mon Feb 22 2021 00:50:58 GMT+0800",
  //   owner: "StoneQi",
  //   repo: "stoneqi.github.io",
  //   oauth: {
  //     client_id:"d9f4e8907ec2566c2307",
  //     client_secret: "2390fe6084c9227566e82de1755e3c3de4a13592",
  //   }
  // })
  // gitment.render('comments')

  const gitalk = new Gitalk({
  clientID: "d9f4e8907ec2566c2307",
  clientSecret: "2390fe6084c9227566e82de1755e3c3de4a13592",
  repo: "stoneqi.github.io",      // The repository of store comments,
  owner: "StoneQi",
  admin: ["StoneQi"],
  language: "zh-CN",
  id: "Mon Feb 22 2021 00:50:58 GMT+0800",      // Ensure uniqueness and length less than 50
  distractionFreeMode: false  // Facebook-like distraction free mode
})

gitalk.render('gitalk-container')
</script>
    </main>
    <a class="not-found">not found!</a>
    <div class="search-items">
    </div>
    <a href="#header" id="top" style="display:none">
        <i class="fa fa-sort-asc fa-2x"></i>
    </a>
    <footer class="footer">
    <div class="footer-copyright">©️2017
    <a href="//github.com/Vevlins/toki" class="link" target="_blank">Toki</a>  by Vevlins
    </div>    
</footer>

    
<script src="/js/jquery.js"></script>

    
<script src="/js/toki.js"></script>
  
  <script>hljs.initHighlightingOnLoad();</script>
</body>
</html>